package dotty.tools
package dotc
package semanticdb

import scala.language.unsafeNulls

import core.*
import Phases.*
import ast.tpd.*
import ast.Trees.{mods, WithEndMarker}
import Contexts.*
import Symbols.*
import Flags.*
import Names.Name
import StdNames.nme
import NameOps.*
import Denotations.StaleSymbol
import util.Spans.Span
import util.SourceFile

import scala.collection.mutable
import scala.annotation.{ threadUnsafe => tu, tailrec }
import scala.jdk.CollectionConverters.*
import scala.PartialFunction.condOpt
import typer.ImportInfo.withRootImports

import dotty.tools.dotc.reporting.Diagnostic.Warning
import dotty.tools.dotc.{semanticdb => s}
import dotty.tools.io.{AbstractFile, JarArchive}
import dotty.tools.dotc.semanticdb.DiagnosticOps.*
import scala.util.{Using, Failure, Success}
import java.nio.file.Path


/** Extract symbol references and uses to semanticdb files.
 *  See https://scalameta.org/docs/semanticdb/specification.html#symbol-1
 *  for a description of the format.
 *
 *  Here, we define two phases for "ExtractSemanticDB", "PostTyper" and "PostInlining".
 *
 *  The "PostTyper" phase extracts SemanticDB information such as symbol
 *  definitions, symbol occurrences, type information, and synthetics
 *  and write .semanticdb file.
 *
 *  The "PostInlining" phase extracts diagnostics from "ctx.reporter" and
 *  attaches them to the SemanticDB information extracted in the "PostTyper" phase.
 *  We need to run this phase after the "CheckUnused.PostInlining" phase
 *  so that we can extract the warnings generated by "-Wunused".
 */
class ExtractSemanticDB private (phaseMode: ExtractSemanticDB.PhaseMode) extends Phase:

  override val phaseName: String = ExtractSemanticDB.phaseNamePrefix + phaseMode.toString()

  override val description: String = ExtractSemanticDB.description

  override def isRunnable(using Context) =
    import ExtractSemanticDB.{semanticdbTarget, outputDirectory}
    def writesToOutputJar = semanticdbTarget.isEmpty && outputDirectory.isInstanceOf[JarArchive]
    (super.isRunnable || ctx.isBestEffort) && ctx.settings.Xsemanticdb.value && !writesToOutputJar

  // Check not needed since it does not transform trees
  override def isCheckable: Boolean = false

  private def computeDiagnostics(
      sourceRoot: String,
      warnings: Map[SourceFile, List[dotty.tools.dotc.reporting.Diagnostic]],
      append: ((Path, List[Diagnostic])) => Unit)(using Context): Boolean = monitor(phaseName) {
    val unit = ctx.compilationUnit
    warnings.get(unit.source).foreach { ws =>
      val outputDir =
        ExtractSemanticDB.semanticdbPath(
          unit.source,
          ExtractSemanticDB.semanticdbOutDir,
          sourceRoot
        )
      append((outputDir, ws.map(_.toSemanticDiagnostic)))
    }
  }

  private def extractSemanticDB(sourceRoot: String, writeSemanticdbText: Boolean)(using Context): Boolean =
    monitor(phaseName) {
      val unit = ctx.compilationUnit
      val outputDir =
        ExtractSemanticDB.semanticdbPath(
          unit.source,
          ExtractSemanticDB.semanticdbOutDir,
          sourceRoot
        )
      val extractor = ExtractSemanticDB.Extractor()
      extractor.extract(unit.tpdTree)
      ExtractSemanticDB.write(
        unit.source,
        extractor.occurrences.toList,
        extractor.symbolInfos.toList,
        extractor.synthetics.toList,
        outputDir,
        sourceRoot,
        writeSemanticdbText
      )
    }

  override def runOn(units: List[CompilationUnit])(using ctx: Context): List[CompilationUnit] = {
    val sourceRoot = ctx.settings.sourceroot.value
    val appendDiagnostics = phaseMode == ExtractSemanticDB.PhaseMode.AppendDiagnostics
    val unitContexts = units.map(ctx.fresh.setCompilationUnit(_).withRootImports)
    if (appendDiagnostics)
      val warningsAndInfos = (ctx.reporter.allWarnings ++ ctx.reporter.allInfos).groupBy(w => w.pos.source)
      val buf = mutable.ListBuffer.empty[(Path, Seq[Diagnostic])]
      val units0 =
        for unitCtx <- unitContexts if computeDiagnostics(sourceRoot, warningsAndInfos, buf += _)(using unitCtx)
        yield unitCtx.compilationUnit
      cancellable {
        buf.toList.asJava.parallelStream().forEach { case (out, diagnostics) =>
          ExtractSemanticDB.appendDiagnostics(diagnostics, out)
        }
      }
      units0
    else
      val writeSemanticdbText = ctx.settings.semanticdbText.value
      for unitCtx <- unitContexts if extractSemanticDB(sourceRoot, writeSemanticdbText)(using unitCtx)
      yield unitCtx.compilationUnit
  }

  def run(using Context): Unit = unsupported("run")
end ExtractSemanticDB

object ExtractSemanticDB:
  import java.nio.file.Path
  import java.nio.file.Files
  import java.nio.file.Paths

  val phaseNamePrefix: String = "extractSemanticDB"
  val description: String = "extract info into .semanticdb files"

  enum PhaseMode:
    case ExtractSemanticInfo
    case AppendDiagnostics

  class ExtractSemanticInfo extends ExtractSemanticDB(PhaseMode.ExtractSemanticInfo)

  class AppendDiagnostics extends ExtractSemanticDB(PhaseMode.AppendDiagnostics)

  private def semanticdbTarget(using Context): Option[Path] =
    Option(ctx.settings.semanticdbTarget.value)
      .filterNot(_.isEmpty)
      .map(Paths.get(_))

  /** Destination for generated classfiles */
  private def outputDirectory(using Context): AbstractFile =
    ctx.settings.outputDir.value

  /** Output directory for SemanticDB files */
  private def semanticdbOutDir(using Context): Path =
    semanticdbTarget.getOrElse(outputDirectory.jpath)

  private def absolutePath(path: Path): Path = path.toAbsolutePath.normalize

  private def write(
    source: SourceFile,
    occurrences: List[SymbolOccurrence],
    symbolInfos: List[SymbolInformation],
    synthetics: List[Synthetic],
    outpath: Path,
    sourceRoot: String,
    semanticdbText: Boolean
  ): Unit =
    Files.createDirectories(outpath.getParent())
    val doc: TextDocument = TextDocument(
      schema = Schema.SEMANTICDB4,
      language = Language.SCALA,
      uri = Tools.mkURIstring(Paths.get(relPath(source, sourceRoot))),
      text = if semanticdbText then String(source.content) else "",
      md5 = internal.MD5.compute(String(source.content)),
      symbols = symbolInfos,
      occurrences = occurrences,
      synthetics = synthetics,
    )
    val docs = TextDocuments(List(doc))
    val out = Files.newOutputStream(outpath)
    try
      val stream = internal.SemanticdbOutputStream.newInstance(out)
      docs.writeTo(stream)
      stream.flush()
    finally
      out.close()
  end write

  private def appendDiagnostics(
    diagnostics: Seq[Diagnostic],
    outpath: Path
  ): Unit =
    Using.Manager { use =>
      val in = use(Files.newInputStream(outpath))
      val sin = internal.SemanticdbInputStream.newInstance(in)
      val docs = TextDocuments.parseFrom(sin)

      val out = use(Files.newOutputStream(outpath))
      val sout = internal.SemanticdbOutputStream.newInstance(out)
      TextDocuments(docs.documents.map(_.withDiagnostics(diagnostics))).writeTo(sout)
      sout.flush()
    } match
      case Failure(ex) => // failed somehow, should we say something?
      case Success(_) => // success to update semanticdb, say nothing
  end appendDiagnostics

  private def relPath(source: SourceFile, sourceRoot: String) =
    SourceFile.relativePath(source, sourceRoot)

  private def semanticdbPath(source: SourceFile, base: Path, sourceRoot: String): Path =
    absolutePath(base)
      .resolve("META-INF")
      .resolve("semanticdb")
      .resolve(relPath(source, sourceRoot))
      .resolveSibling(source.name + ".semanticdb")

  /** Extractor of symbol occurrences from trees */
  class Extractor extends TreeTraverser:
    import Scala3.{_, given}
    given s.SemanticSymbolBuilder = s.SemanticSymbolBuilder()
    val synth = SyntheticsExtractor()
    given converter: s.TypeOps = s.TypeOps()

    /** The bodies of synthetic locals */
    private val localBodies = mutable.HashMap[Symbol, Tree]()

    /** The extracted symbol occurrences */
    val occurrences = new mutable.ListBuffer[SymbolOccurrence]()

    /** The extracted symbol infos */
    val symbolInfos = new mutable.ListBuffer[SymbolInformation]()

    val synthetics = new mutable.ListBuffer[s.Synthetic]()

    /** A cache of localN names */
    val localNames = new mutable.HashSet[String]()

    /** The symbol occurrences generated so far, as a set */
    private val generated = new mutable.HashSet[SymbolOccurrence]

    def extract(tree: Tree)(using Context): Unit =
      traverse(tree)
      val fakeSyms = converter.fakeSymbols.map(_.symbolInfo(Set.empty)(using LinkMode.SymlinkChildren, converter))
      symbolInfos.appendAll(fakeSyms)

    /** Definitions of this symbol should be excluded from semanticdb */
    private def excludeDef(sym: Symbol)(using Context): Boolean =
      !sym.exists
      || sym.isLocalDummy
      // basically do not register synthetic symbols, except anonymous class
      // `new Foo { ... }`
      || (sym.is(Synthetic) && !sym.isAnonymousClass)
      || sym.isSetter
      || sym.isOldStyleImplicitConversion(forImplicitClassOnly = true)
      || sym.owner.isGivenInstanceSummoner
      || excludeDefOrUse(sym)

    private def excludeDefOrUse(sym: Symbol)(using Context): Boolean =
      !sym.exists
      || sym.name.is(NameKinds.DefaultGetterName)
      || sym.isConstructor && (sym.owner.is(ModuleClass) || !sym.isGlobal)
      || excludeSymbol(sym)

    private def excludeSymbol(sym: Symbol)(using Context): Boolean =
      !sym.exists
      || sym.is(PhantomSymbol)
      || sym.name.isWildcard
      || excludeQual(sym)

    private def excludeQual(sym: Symbol)(using Context): Boolean =
      !sym.exists
      || sym.isAnonymousFunction
      || sym.isAnonymousModuleVal
      || sym.name.isEmptyNumbered

    private def excludeChildren(sym: Symbol)(using Context): Boolean =
      !sym.exists
      || sym.is(Param) && sym.info.bounds.hi.isInstanceOf[Types.HKTypeLambda]
      || sym.isOldStyleImplicitConversion(forImplicitClassOnly = true)

    /** Uses of this symbol where the reference has given span should be excluded from semanticdb */
    private def excludeUse(qualifier: Option[Symbol], sym: Symbol)(using Context): Boolean =
      !sym.exists
      || excludeDefOrUse(sym)
      || sym.isConstructor && sym.owner.isAnnotation
      || sym == defn.Any_typeCast
      || sym.owner == defn.OpsPackageClass
      || qualifier.exists(excludeQual)

    /** This block is created by lifting i.e. EtaExpansion */
    private def isProbablyLifted(block: Block)(using Context) =
      def isSyntheticDef(t: Tree) =
        t match
          case t: (ValDef | DefDef) => t.symbol.isSyntheticWithIdent
          case _ => false
      block.stats.forall(isSyntheticDef)

    private def traverseAnnotsOfDefinition(sym: Symbol)(using Context): Unit =
      for annot <- sym.annotations do
        if annot.tree.span.exists
        && annot.tree.span.hasLength then
          annot.tree match
            case tree: Typed => () // hack for inline code
            case tree        => traverse(tree)

    override def traverse(tree: Tree)(using Context): Unit =

      tree match
        case tree: DefTree if tree.symbol.exists =>
          traverseAnnotsOfDefinition(tree.symbol)
        case _ =>
          ()

      tree match
        case tree: PackageDef =>
          tree.stats.foreach(traverse)
          if !excludeDef(tree.pid.symbol) && tree.pid.span.hasLength then
            tree.pid match
              case tree: Select =>
                traverse(tree.qualifier)
                registerDefinition(tree.symbol, selectSpan(tree), Set.empty, tree.source)
              case tree => registerDefinition(tree.symbol, tree.span, Set.empty, tree.source)
        case tree: NamedDefTree =>
          if tree.symbol.exists && !tree.symbol.isAllOf(ModuleValCreationFlags) then
            tree match {
              case tree: ValDef if tree.symbol.isAllOf(EnumValue) =>
                tree.rhs match
                case Block(TypeDef(_, template: Template) :: _, _) => // simple case with specialised extends clause
                  template.parents.filter(!_.span.isZeroExtent).foreach(traverse)
                case _ => // calls $new
              case tree: ValDef if tree.symbol.isSelfSym =>
                if tree.tpt.span.hasLength then
                  traverse(tree.tpt)
              case tree: DefDef if tree.symbol.isConstructor => // ignore typeparams for secondary ctors
                tree.trailingParamss.foreach(_.foreach(traverse))
                traverse(tree.rhs)
              case tree: (DefDef | ValDef) if tree.symbol.isSyntheticWithIdent =>
                tree match
                  case tree: DefDef =>
                    tree.paramss.foreach(_.foreach(param => registerSymbolSimple(param.symbol)))
                  case tree: ValDef if tree.symbol.is(Given) =>
                    traverse(tree.tpt)
                  case _ =>
                if !tree.symbol.isGlobal then
                  localBodies(tree.symbol) = tree.rhs
                // ignore rhs

              case PatternValDef(pat, rhs) =>
                traverse(rhs)
                PatternValDef.collectPats(pat).foreach(traverse)
              case tree: TypeDef =>
                traverseChildren(tree)
              case tree =>
                if !excludeChildren(tree.symbol) then
                  traverseChildren(tree)
            }
            if !excludeDef(tree.symbol) && (tree.span.hasLength || tree.symbol.isAnonymousClass) then
              registerDefinition(tree.symbol, tree.nameSpan, symbolKinds(tree), tree.source)
              val privateWithin = tree.symbol.privateWithin
              if privateWithin.exists then
                registerUseGuarded(None, privateWithin, spanOfSymbol(privateWithin, tree.span, tree.source), tree.source)
            else if !excludeSymbol(tree.symbol) then
              registerSymbol(tree.symbol, symbolKinds(tree))
        case tree: Template if tree.symbol != NoSymbol && tree.symbol.owner.is(Invisible) =>
          // do nothing
          // exclude the symbols and synthetics generated by @main annotation
          // (main class generated by @main has `Invisible` flag, see `MainProxies.scala`).
        case tree: Template =>
          val ctorSym = tree.constr.symbol
          for parent <- tree.parentsOrDerived if parent.span.hasLength do
            traverse(parent)
          val selfSpan = tree.self.span
          if selfSpan.exists && selfSpan.hasLength then
            traverse(tree.self)
          if tree.symbol != NoSymbol && tree.symbol.owner.isEnumClass then
            tree.body.foreachUntilImport(traverse).foreach(traverse) // the first import statement
          else
            tree.body.foreach(traverse)
          if !excludeDef(ctorSym) then
            traverseAnnotsOfDefinition(ctorSym)
            ctorParams(tree.constr.termParamss, tree.constr.leadingTypeParams, tree.body)
            registerDefinition(ctorSym, tree.constr.nameSpan.startPos, Set.empty, tree.source)
        case tree: Apply if tree.fun.symbol.exists =>
          @tu lazy val genParamSymbol: Name => String = tree.fun.symbol.funParamSymbol
          traverse(tree.fun)
          synth.tryFindSynthetic(tree).foreach(synthetics.addOne)
          for arg <- tree.args do
            arg match
              case tree @ NamedArg(name, arg) =>
                traverse(localBodies.get(arg.symbol).getOrElse(arg))
                registerUse(genParamSymbol(name), tree.span.startPos.withEnd(tree.span.start + name.toString.length), tree.source)
              case _ => traverse(arg)
        case tree: Assign =>
          val qualSym = condOpt(tree.lhs) { case Select(qual, _) if qual.symbol.exists => qual.symbol }
          if !excludeUse(qualSym, tree.lhs.symbol) then
            val lhs = tree.lhs.symbol
            val setter = lhs.matchingSetter.orElse(lhs)
            tree.lhs match
              case tree: Select => registerUse(setter, selectSpan(tree), tree.source)
              case tree         => registerUse(setter, tree.span, tree.source)
            traverseChildren(tree.lhs)
          traverse(tree.rhs)
        case tree: Ident =>
          if tree.name != nme.WILDCARD then
            val sym = tree.symbol.adjustIfCtorTyparam
            registerUseGuarded(None, sym, tree.span, tree.source)
        case tree: Select =>
          val qual = tree.qualifier
          val qualSpan = qual.span
          val sym = tree.symbol.adjustIfCtorTyparam
          if qualSpan.exists && qualSpan.hasLength then
            traverse(qual)
          if (sym != NoSymbol)
            registerUseGuarded(qual.symbol.ifExists, sym, selectSpan(tree), tree.source)
          else
            qual.symbol.info.lookupSym(tree.name).foreach(sym =>
              registerUseGuarded(qual.symbol.ifExists, sym, selectSpan(tree), tree.source)
            )
        case tree: Import =>
          if tree.span.exists && tree.span.hasLength then
            traverseChildren(tree)
            for sel <- tree.selectors do
              val imported = sel.imported.name
              if imported != nme.WILDCARD then
                for alt <- tree.expr.tpe.member(imported).alternatives do
                  registerUseGuarded(None, alt.symbol, sel.imported.span, tree.source)
                  try
                    if (alt.symbol.companionClass.exists)
                      registerUseGuarded(None, alt.symbol.companionClass, sel.imported.span, tree.source)
                  catch case ex: StaleSymbol =>
                    // can happen for constructor proxies. Test case is pos-macros/i13532.
                    ()

        case tree: Inlined =>
          traverse(tree.call)

        case tree: TypeApply =>
          synth.tryFindSynthetic(tree).foreach(synthetics.addOne)
          traverseChildren(tree)

        case tree: TypeTree =>
          tree.typeOpt match
            // Any types could be appear inside of `TypeTree`, but
            // types that precent in source other than TypeRef are traversable and contain Ident tree nodes
            // (e.g. TypeBoundsTree, AppliedTypeTree)
            case Types.TypeRef(_, sym: Symbol) if namePresentInSource(sym, tree.span, tree.source) =>
              registerUseGuarded(None, sym, tree.span, tree.source)
            case _ => ()

        // If tree is lifted, ignore Synthetic status on all the definitions and traverse all childrens
        case tree: Block if isProbablyLifted(tree) =>
          tree.stats.foreach:
            case t: (ValDef | DefDef) if !excludeChildren(t.symbol) => traverseChildren(t)
            case _ => ()
          traverse(tree.expr)

        case _ =>
          traverseChildren(tree)

      tree match
        case tree: WithEndMarker[t] =>
          val endSpan = tree.endSpan
          if endSpan.exists &&
            namePresentInSource(tree.symbol, endSpan, tree.source) then
            // non-symbol end marker shouldn't have Symbol Occurrence
            registerUseGuarded(None, tree.symbol, endSpan, tree.source)
        case _ =>

    end traverse

    private object PatternValDef:

      def unapply(tree: ValDef)(using Context): Option[(Tree, Tree)] = tree.rhs match

        case Match(Typed(selected: Tree, tpt: TypeTree), CaseDef(pat: Tree, _, _) :: Nil)
        if tpt.span.exists && !tpt.span.hasLength && tpt.tpe.isAnnotatedByUncheckedOrRuntimeChecked =>
          Some((pat, selected))

        case _ => None

      extension (tpe: Types.Type)
        private inline def isAnnotatedByUncheckedOrRuntimeChecked(using Context) = tpe match
          case Types.AnnotatedType(_, annot) =>
            annot.symbol == defn.UncheckedAnnot || annot.symbol == defn.RuntimeCheckedAnnot
          case _                             => false

      def collectPats(pat: Tree): List[Tree] =

        @tailrec
        def impl(acc: List[Tree], pats: List[Tree]): List[Tree] = pats match

          case pat::pats => pat match
            case Typed(UnApply(fun: Tree, _, args), tpt: Tree) => impl(fun::tpt::acc, args:::pats)
            case Typed(obj: Ident, tpt: Tree)                  => impl(obj::tpt::acc, pats)
            case UnApply(fun: Tree, _, args)                   => impl(fun::acc,      args:::pats)
            case obj: Ident                                    => impl(obj::acc,      pats)
            case _                                             => impl(acc,           pats)

          case Nil => acc

        impl(Nil, pat::Nil)

    end PatternValDef



    private def registerSymbol(sym: Symbol, symkinds: Set[SymbolKind])(using Context): Unit =
      val sname = sym.symbolName
      val isLocal = sname.isLocal
      if !isLocal || !localNames.contains(sname) then
        if isLocal then
          localNames += sname
        symbolInfos += sym.symbolInfo(symkinds)(using LinkMode.SymlinkChildren, converter)

    private def registerSymbolSimple(sym: Symbol)(using Context): Unit =
      registerSymbol(sym, Set.empty)

    private def registerOccurrence(symbol: String, span: Span, role: SymbolOccurrence.Role, treeSource: SourceFile)(using Context): Unit =
      val occ = SymbolOccurrence(range(span, treeSource), symbol, role)
      if !generated.contains(occ) && occ.symbol.nonEmpty then
        occurrences += occ
        generated += occ

    private def registerUseGuarded(qualSym: Option[Symbol], sym: Symbol, span: Span, treeSource: SourceFile)(using Context) =
      if !excludeUse(qualSym, sym) && !span.isZeroExtent then
        registerUse(sym, span, treeSource)

    private def registerUse(sym: Symbol, span: Span, treeSource: SourceFile)(using Context): Unit =
      registerUse(sym.symbolName, span, treeSource)

    private def registerUse(symbol: String, span: Span, treeSource: SourceFile)(using Context): Unit =
      registerOccurrence(symbol, span, SymbolOccurrence.Role.REFERENCE, treeSource)

    private def registerDefinition(sym: Symbol, span: Span, symkinds: Set[SymbolKind], treeSource: SourceFile)(using Context) =
      val sname = sym.symbolName
      val finalSpan = if !span.hasLength || !sym.is(Given) || namePresentInSource(sym, span, treeSource) then
        span
      else
        Span(span.start)

      if namePresentInSource(sym, span, treeSource) || sym.isAnonymousClass then
        registerOccurrence(sname, finalSpan, SymbolOccurrence.Role.DEFINITION, treeSource)
      if !sym.is(Package) then
        registerSymbol(sym, symkinds)

    private def spanOfSymbol(sym: Symbol, span: Span, treeSource: SourceFile)(using Context): Span =
      val contents = if treeSource.exists then treeSource.content() else Array.empty[Char]
      val idx = contents.indexOfSlice(sym.name.show, span.start)
      val start = if idx >= 0 then idx else span.start
      Span(start, start + sym.name.show.length, start)

    extension (list: List[List[ValDef]])
      private  inline def isSingleArg = list match
        case (_::Nil)::Nil => true
        case _             => false

    extension (tree: DefDef)
      private def isSetterDef(using Context): Boolean =
        tree.name.isSetterName && tree.mods.is(Accessor) && tree.termParamss.isSingleArg

    private def findGetters(ctorParams: Set[Names.TermName], body: List[Tree])(using Context): Map[Names.TermName, ValDef] =
      if ctorParams.isEmpty || body.isEmpty then
        Map.empty
      else
        body.collect({
          case tree: ValDef
          if ctorParams.contains(tree.name)
          && !tree.symbol.isPrivate =>
            tree.name -> tree
        }).toMap
    end findGetters

    private def selectSpan(tree: Select)(using Context) =
      val end = tree.span.end
      val limit = tree.qualifier.span.end
      if limit < end then
        tree.nameSpan
      else Span(limit, end)

    extension (span: Span)
      private def hasLength: Boolean = span.exists && !span.isZeroExtent

    /**Consume head while not an import statement.
     * Returns the rest of the list after the first import, or else the empty list
     */
    extension (body: List[Tree])
      @tailrec private def foreachUntilImport(op: Tree => Unit): List[Tree] = body match
        case ((_: Import) :: rest) => rest
        case stat :: rest =>
          op(stat)
          rest.foreachUntilImport(op)
        case Nil => Nil

    extension (sym: Symbol)
      private def adjustIfCtorTyparam(using Context) =
        if sym.isType && sym.owner.exists && sym.owner.isConstructor then
          matchingMemberType(sym, sym.owner.owner)
        else
          sym

    private inline def matchingMemberType(ctorTypeParam: Symbol, classSym: Symbol)(using Context) =
      classSym.info.member(ctorTypeParam.name).symbol

    /**Necessary because not all of the eventual flags are propagated from the Tree to the symbol yet.
     */
    private def symbolKinds(tree: NamedDefTree)(using Context): Set[SymbolKind] =
      if tree.symbol.isSelfSym then
        Set.empty
      else
        val symkinds = mutable.HashSet.empty[SymbolKind]
        tree match
          case tree: ValDef =>
            if !tree.symbol.is(Param) then
              symkinds += (if tree.mods.is(Mutable) then SymbolKind.Var else SymbolKind.Val)
            if tree.rhs.isEmpty && !tree.symbol.isOneOf(TermParam | CaseAccessor | ParamAccessor) then
              symkinds += SymbolKind.Abstract
          case tree: DefDef =>
            if tree.isSetterDef then
              symkinds += SymbolKind.Setter
            else if tree.rhs.isEmpty then
              symkinds += SymbolKind.Abstract
          // if symbol isType, it's type variable
          case tree: Bind if (!tree.symbol.isType) =>
            symkinds += SymbolKind.Val
          case tree: Bind if (tree.symbol.isType) =>
            symkinds += SymbolKind.TypeVal
          case _ =>
        symkinds.toSet

    private def ctorParams(
      vparamss: List[List[ValDef]], tparams: List[TypeDef], body: List[Tree])(using Context): Unit =
      @tu lazy val getters = findGetters(vparamss.flatMap(_.map(_.name)).toSet, body)
      for
        vparams <- vparamss
        vparam  <- vparams
      do
        traverse(vparam.tpt)
        if !excludeSymbol(vparam.symbol) then
          traverseAnnotsOfDefinition(vparam.symbol)
          val symkinds =
            getters.get(vparam.name).fold(SymbolKind.emptySet)(getter =>
              if getter.mods.is(Mutable) then SymbolKind.VarSet else SymbolKind.ValSet)
          registerSymbol(vparam.symbol, symkinds)
        traverse(vparam.tpt)
      tparams.foreach(tp => traverse(tp.rhs))
  end Extractor
end ExtractSemanticDB
